package org.mtmi.ui.controls;

import java.util.ArrayList;
import java.util.HashMap;

import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.mtmi.ui.GraphicUtil;
import org.mtmi.ui.IAdaptable;
import org.mtmi.ui.M;
import org.mtmi.ui.controls.listeners.ControlBoundsEvent;
import org.mtmi.ui.controls.listeners.ControlBoundsListener;
import org.mtmi.ui.controls.listeners.ControlDisposedListener;
import org.mtmi.ui.controls.listeners.DisposeEvent;
import org.mtmi.ui.layouts.LayoutData;
import org.mtmi.ui.scenes.Scene;

/**
 * Abstract class root of all visual elements.
 * @author jgout
 *
 */
public abstract class Control implements IVisualControl, IAdaptable
{

	public static final int borderWidth = 4;
	
	protected Point location;
	protected Point size;
//	protected Path outline;
	protected Scene scene;
	private Color fgColor;
	private Color bgColor;
	private HashMap<Object,Object> datas;
	
	protected int flags;
	private ArrayList<ControlDisposedListener> lis_disposed;
	
	public int getFlags() { return flags; }
	
	public Color getForeground() {
		return fgColor;
	}

	public void setForeground(Color fgColor) {
		this.fgColor = fgColor;
	}

	public Color getBackground() {
		return bgColor;
	}

	public void setBackground(Color bgColor) {
		this.bgColor = bgColor;
	}

	public Control(Scene scene,int _flags ) {
		this.scene = scene;
		location = new Point(0,0);
		size = new Point(20,20);
		flags = _flags;
		scene.addControl( this );
	}
	
	/**
	 * Removes receiver form its parent (scene) element list.
	 */
	public void dispose() {
		//note: the scene will notify dispose listeners
		scene.disposeControl( this );
		flags |= M.DISPOSED;
		redraw(); //repaint area covered by me
	}
	
	/**
	 * @return true if control has been disposed
	 */
	public boolean isDisposed() {
		return (flags & M.DISPOSED)==M.DISPOSED;
	}
	
	public boolean isSceneControl() { return (flags & M.SCENE_EDITOR_CONTROL)==M.SCENE_CONTROL; }
	public boolean isSceneEditorControl() { return (flags & M.SCENE_EDITOR_CONTROL)==M.SCENE_EDITOR_CONTROL; }
	
	public Scene getScene() {
		return scene;
	}
	
	@Override
	public Point getLocation() {
		return location;
	}
	
	/** change location of control, this method call setBounds() */
	final public void setLocation( int x, int y ) {
		setBounds( x, y , size.x, size.y );
	}
	/** change location of control, this method call setBounds() */
	final public void setLocation( Point loc ) {
		setBounds( loc.x, loc.y, size.x, size.y );
	}

	@Override
	public Point getSize() {
		return size;
	}
	
	/** change control size, this method calls setBounds() */
	final public void setSize( int width, int height ) {
		setBounds( location.x, location.y, width, height );
	}
	/** change control size, this method calls setBounds(). */
	final public void setSize( Point s ) {
		setBounds( location.x, location.y, s.x, s.y );
	}
	
	/** change control bounds */
	public void setBounds( int x, int y, int w, int h ) {
		int ox=location.x, oy=location.y;
		int ow=size.x, oh=size.y;
		
		location.x = x; location.y =y;
		size.x = w; size.y = h;
		
		if( ox!=x || oy!=y || ow!=w || oh!=h ) {
			notifyBoundsChanged( ox, oy, ow, oh );
		}
	}
	
	/** convenience method, call <code>setBounds( r.x, r.y, r.width, r.height )</code> */
	public final void setBounds( Rectangle r ) {
		setBounds( r.x, r.y, r.width, r.height );
	}
	
	/** @return control bounds, if r is not null use it to return value, otherwise create new rectangle */
	public Rectangle getBounds( Rectangle r ) {
		if( r==null ) {
			return new Rectangle( location.x, location.y, size.x, size.y );
		} else {
			r.x = location.x; r.y = location.y;
			r.width = size.x; r.height =size.y;
			return r;
		}
	}

	public void addControlDisposedListener( ControlDisposedListener lis ) {
		if( lis_disposed == null ) {
			lis_disposed = new ArrayList<ControlDisposedListener>();
		}
		lis_disposed.add( lis );
	}
	
	public void removeControlDisposedListener( ControlDisposedListener lis ) {
		if( lis_disposed != null ) {
			lis_disposed.remove( lis );
		}
	}
	
	public void notifyDisposedListeners() {
		if( lis_disposed!=null ) {
			DisposeEvent event = new DisposeEvent( this );
			ControlDisposedListener cdls[] = lis_disposed.toArray( new ControlDisposedListener[lis_disposed.size()] );
			for( int i=0; i<cdls.length; i++ ) {
				cdls[i].controlDisposed( event );
			}
		}
	}
	
	public Object getData( Object key ) {
		if( datas==null ) {
			return null;
		} else {
			return datas.get( key );
		}
	}
	
	/** @return boolean data associated to key, or default value if there are no data or data is not a boolean */
	public boolean getData( Object key, boolean default_value ) {
		if( datas==null ) return default_value;
		Object d = datas.get( key );
		if( d instanceof Boolean ) {
			return ((Boolean)d).booleanValue();
		} else {
			return default_value;
		}
	}
	
	/** @return boolean data associated to key, or default value if there are no data or data is not an integer */
	public int getData( Object key, int default_value ) {
		if( datas==null ) return default_value;
		Object d = datas.get( key );
		if( d instanceof Integer ) {
			return ((Integer)d).intValue();
		} else {
			return default_value;
		}
	}
	
	/** add a data to control's map using given key, any existing key is overriden.
	 * To remove a data use {@link #removeData(Object)}.
	 * @param key the key used to retrieve data ({@link #getData(Object)}
	 * @param data the data to store.
	 */
	public void setData( Object key, Object data ) {
		if( datas==null ) {
			datas = new HashMap<Object, Object>();
		}
		datas.put( key, data );
	}
	
	/** remove the data with the given key from control's data map */
	public void removeData( Object key ) {
		if( datas!=null ) {
			datas.remove( key );
		}
	}
	
	/** @return the preferred size for the control in the given area (may be 0x0) */
	public abstract Point computeSize( LayoutData l );
	
	/** move the control to another scene */
	public void setScene( Scene _scene ) {
		if( scene!= null ) {
			scene.removeControl( this );
		}
		scene = _scene;
		if( scene != null ) {
			scene.addControl( this );
		}
	}
	
	private ArrayList<ControlBoundsListener> lis_bounds;
	
	public void addControlBoundsListener( ControlBoundsListener lis ) {
		if( lis_bounds == null ) {
			lis_bounds = new ArrayList<ControlBoundsListener>();
		}
		lis_bounds.add( lis );
	}
	
	public void removeControlBoundsListener( ControlBoundsListener lis ) {
		if( lis_bounds != null ) {
			lis_bounds.remove( lis );
		}
	}
	
	protected void notifyBoundsChanged( int ox, int oy, int ow, int oh ) {
		if( lis_bounds != null ) {
			ControlBoundsListener lis[] = lis_bounds.toArray( new ControlBoundsListener[ lis_bounds.size() ] );
			ControlBoundsEvent e = new ControlBoundsEvent( this, ox, oy, ow, oh );
			for( int i=0; i<lis.length; i++ ) {
				lis[i].controlBoundsChanged( e );
			}
		}
	}
	
	/** On Control.class return this, otherwise return null */
	public Object getAdapter( Class<?> clazz, Object arg ) {
		if( clazz == Control.class ) {
			return this;
		}
		return null;
	}

	/**
	 * @return the area to redraw when control is needed to be repaint, by default it's the control bounds.
	 * subclasses may override this method to add or change area to be redraw depending on control's state
	 * (for example a link to another control, ... ).
	 * @see GraphicUtil#setRect(Rectangle, int, int, int, int)
	 * @param r a rectangle already allocated (or null), that can be used whereas allocating a new one. 
	 * @return the area to redraw.
	 */
	public Rectangle getRedrawArea( Rectangle r ) {
		return GraphicUtil.setRect( r, location.x, location.y, size.x, size.y );
	}
	
	/**
	 * Mark control to be redraw, by default the rectangle defined by control's bounds is marked to be refreshed.
	 * subclasses may override this method to change the area redrawn. */
	final public void redraw() {
//TODO: getScene().redraw();
		Rectangle r = getRedrawArea( null );
		getScene().redraw( r.x, r.y, r.width, r.height, true );
	}
}
